const Canva = {
    Framerate: 30,
    Terrain: null,
};  // Basic Canva Config

const World = {
    Camera: null,
    Radius: 1200,
    Detail: 30,
    Height: 600,
    MaxObj: 7,
};  // Basic World Config

const Materials = {
    Water: {Color: null, Alpha: null},
    Bush: {Color: null, Alpha: null},
    Moss: {Color: null, Alpha: null},
    Grass: {Color: null, Alpha: null},
    Soil: {Color: null, Alpha: null},
    Sand: {Color: null, Alpha: null},
    Stone: {Color: null, Alpha: null},
    Rock: {Color: null, Alpha: null},
    Mud: {Color: null, Alpha: null},
};  // Basic Materials

class MeshTerrain {
    constructor(radius, detail, height, maxObj) {

        this.radius = radius;
        this.detail = detail;
        this.height = height;
        this.maxObj = maxObj;
        this.patch = 2*radius/detail;
        this.meshAnchors = [];
        this.meshPatches = [];
        this.meshObjects = [];
        this.meshActions = [];
        this.meshColours = [];

        // Generate Terrain Mesh Anchors
        for (let iteD = 0; iteD <= detail; iteD++) {
            for (let iteW = 0; iteW <= detail; iteW++) {
                const X = lerp(-radius, radius, iteW/detail);
                const Z = lerp(-radius, radius, iteD/detail);

                let Y = map(noise(0.26*X/this.patch, 0.32*Z/this.patch), 0, 1, -height/28, height/15);
                Y += map(noise(0.16*X/this.patch, 0.2*Z/this.patch), 0, 1, -height/16, height/27);
                if (Y > 0) Y *= map(Math.random(), 0, 1, 1.4, 1.8);
                else Y *= map(Math.random(), 0, 1, 1.0, 1.2);

                this.meshAnchors.push([X, Y, Z]);
            }   // Generate Mesh Anchors along X Axis
        }   // Generate Mesh Anchors along Z Axis

        // Generate Terrain Mesh Patches
        for (let iteD = 0; iteD < detail; iteD++) {
            for (let iteW = 0; iteW < detail; iteW++) {
                this.meshPatches.push([
                    this.meshAnchors[iteD*(detail + 1) + iteW],
                    this.meshAnchors[(iteD + 1)*(detail + 1) + iteW],
                    this.meshAnchors[iteD*(detail + 1) + iteW + 1],
                    this.meshAnchors[(iteD + 1)*(detail + 1) + iteW + 1],
                ]); // Preload Mesh Patches
            }   // Generate Mesh Patches along X Axis
        }   // Generate Mesh Patches along Z Axis

        for (let iteD = 0; iteD < detail; iteD++) {
            for (let iteW = 0; iteW < detail; iteW++) {
                const objCt = Math.round(Math.random()*maxObj);
                const patch = this.meshPatches[iteD*detail + iteW];
                const meshObject = [];
                for (let iteO = 0; iteO < objCt; iteO++) {
                    const X = lerp(-radius, radius, iteW/detail)
                            + map(Math.random(), 0, 1, 0.08*detail, 0.92*detail);
                    const Z = lerp(-radius, radius, iteD/detail)
                            + map(Math.random(), 0, 1, 0.08*detail, 0.92*detail);

                    const slope = (Z - patch[1][2])/(X - patch[1][0]);
                    const ktile = (slope >= -1) ? patch[3] : patch[0];

                    const dist1 = dist(X, Z, patch[1][0], patch[1][2]);
                    const dist2 = dist(X, Z, ktile[0], ktile[2]);
                    const dist3 = dist(X, Z, patch[2][0], patch[2][2]);
                    const array = [[dist1, patch[1][1]], [dist2, ktile[1]], [dist3, patch[2][1]]];

                    const pro = array.reduce((pro, item) => pro + item[0]*item[1], 0);
                    const sum = array.reduce((sum, item) => sum + item[0], 0);
                    if (pro/sum < -4) meshObject.push([X, pro/sum, Z]);
                }   // Generate a Single Mesh Object
                this.meshObjects.push(meshObject);
                this.meshActions.push([]);

                const average = patch.reduce((sum, item) => sum + item[1], 0)/4;
                const variant = [
                    12*(Math.random() - 0.5),
                    10*(Math.random() - 0.5),
                    11*(Math.random() - 0.5)
                ];  // Add Color Viberation

                if (average < -24) this.meshColours.push([Materials.Bush.Color, variant]);
                else if (average < -18) this.meshColours.push([Materials.Moss.Color, variant]);
                else if (average < -10) this.meshColours.push([Materials.Grass.Color, variant]);
                else if (average < -2 ) this.meshColours.push([Materials.Soil.Color, variant]);
                else if (average <  10) this.meshColours.push([Materials.Sand.Color, variant]);
                else if (average <  16) this.meshColours.push([Materials.Stone.Color, variant]);
                else if (average <  24) this.meshColours.push([Materials.Rock.Color, variant]);
                else this.meshColours.push([Materials.Mud.Color, variant]);
            }   // Generate Mesh Objects along X Axis
        }   // Generate Mesh Objects along Z Axis

    }   // Construct Procedural Map

    render() {

        push(); // Preserve Current Coordination

        stroke(0); strokeWeight(0.4);
        for (let iteD = 0; iteD < this.detail; iteD++) {
            let shift = iteD*this.detail;
            for (let iteW = 0; iteW < this.detail; iteW++) {
                const tile = this.meshPatches[shift + iteW];

                const colour = this.meshColours[shift + iteW];
                const dColor = colour[0].color();
                fill(
                    colour[1][0] + 256*dColor._array[0],
                    colour[1][1] + 256*dColor._array[1],
                    colour[1][2] + 256*dColor._array[2],
                );  // Fill Color
                quad(
                    tile[0][0], tile[0][1], tile[0][2],
                    tile[1][0], tile[1][1], tile[1][2],
                    tile[3][0], tile[3][1], tile[3][2],
                    tile[2][0], tile[2][1], tile[2][2],
                );  // Render Terrain Patch
            }   // Render Each Column
        }   // Render Mesh Terrain

        pop();  // Restore Original Coordination

/*
        noStroke(); fill(Materials.Grass.Color.color());

        for (let iteD = 0; iteD < this.detail; iteD++) {
            for (let iteW = 0; iteW < this.detail; iteW++) {
                const meshObject = this.meshObjects[iteD*this.detail + iteW];
                for (let iteO = 0; iteO < meshObject.length; iteO++) {
                    push(); // Preserve Current Coordination
                    translate(meshObject[iteO][0], meshObject[iteO][1] - 7, meshObject[iteO][2]);
                    rotateZ(Math.PI); cone(2, 14);
                    pop();  // Restore Original Coordination
                }   // Render a Single Mesh Object
            }   // Render Mesh Objects along X Axis
        }   // Render Mesh Objects along Z Axis
*/
    }   // Render Low Poly Terrain

    surface(wCor, dCor) {

        // Restrict the Range of Horizontal Coordination
        if (wCor < -this.radius) wCor = -this.radius;
        if (wCor >= this.radius) wCor =  this.radius - 1;
        if (dCor < -this.radius) dCor = -this.radius;
        if (dCor >= this.radius) dCor =  this.radius - 1;

        const iteW = Math.floor((wCor + this.radius)/this.patch);
        const iteD = Math.floor((dCor + this.radius)/this.patch);

        // Fetch Anchor Information to Caculate Surface Altitude
        const patch = this.meshPatches[iteD*this.detail + iteW];
        const slope = (dCor - patch[1][2])/(wCor - patch[1][0]);
        const ktile = (slope >= -1) ? patch[3] : patch[0];

        const dist1 = dist(wCor, dCor, patch[1][0], patch[1][2]);
        const dist2 = dist(wCor, dCor, ktile[0], ktile[2]);
        const dist3 = dist(wCor, dCor, patch[2][0], patch[2][2]);
        const array = [[dist1, patch[1][1]], [dist2, ktile[1]], [dist3, patch[2][1]]];

        const pro = array.reduce((pro, item) => pro + item[0]*item[1], 0);
        const sum = array.reduce((sum, item) => sum + item[0], 0);

        return (pro/sum > 0) ? 0 : -pro/sum;

    }   // Return the Height of the Surface

}   // Low Poly Terrain

function setup() {
    // Setup the Canvas for Sketching
    createCanvas(innerWidth, innerHeight, WEBGL);

    // Setup Viewpoint for Camera
    World.Camera = createCamera();
    World.Camera.setPosition(1.2*World.Radius, -0.7*1.4*World.Radius, 1.3*World.Radius);
    World.Camera.lookAt(0, 0, 0);

    // Setup Environment Configurations
    angleMode(RADIANS);
    frameRate(Canva.Framerate);

    // Setup Color Configuration
    Materials.Bush.Color = createColorPicker("#063C06");
    Materials.Bush.Color.position(20, 20);
    Materials.Bush.Color.size(24, 24);

    Materials.Moss.Color = createColorPicker("#136303");
    Materials.Moss.Color.position(20, 50);
    Materials.Moss.Color.size(24, 24);

    Materials.Grass.Color = createColorPicker("#1B551B");
    Materials.Grass.Color.position(20, 80);
    Materials.Grass.Color.size(24, 24);

    Materials.Soil.Color = createColorPicker("#624032");
    Materials.Soil.Color.position(20, 110);
    Materials.Soil.Color.size(24, 24);

    Materials.Sand.Color = createColorPicker("#9C9581");
    Materials.Sand.Color.position(20, 140);
    Materials.Sand.Color.size(24, 24);

    Materials.Stone.Color = createColorPicker("#5C5B4D");
    Materials.Stone.Color.position(20, 170);
    Materials.Stone.Color.size(24, 24);

    Materials.Rock.Color = createColorPicker("#2B2412");
    Materials.Rock.Color.position(20, 200);
    Materials.Rock.Color.size(24, 24);

    Materials.Mud.Color = createColorPicker("#1C1303");
    Materials.Mud.Color.position(20, 230);
    Materials.Mud.Color.size(24, 24);

    Materials.Water.Color = createColorPicker("#69B6DD");
    Materials.Water.Color.position(20, 260);
    Materials.Water.Color.size(24, 24);
    Materials.Water.Alpha = createSlider(0, 255, 148, 1);
    Materials.Water.Alpha.position(50, 260);
    Materials.Water.Alpha.style("width", "100px");


    Canva.Terrain = new MeshTerrain(
        World.Radius,
        World.Detail,
        World.Height,
        World.MaxObj,
    );  // Generate Terrain for the World

}   // Setup the Environment

function draw() {
    background(60); orbitControl();

    Canva.Terrain.render();

    noStroke(); fill(
        256*Materials.Water.Color.color()._array[0],
        256*Materials.Water.Color.color()._array[1],
        256*Materials.Water.Color.color()._array[2],
        Materials.Water.Alpha.value()
    );  // Interface for Color Debugging
    rotateX(PI/2); plane(2*World.Radius);

}   // Render the Scenario
